package plugin;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandlerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import core.exception.NotFoundException;

/**
 * This class is responsible for storing a registry of all installed plugins, as well as spawning
 * instances of any particular plugin as needed.
 * @author Royce
 *
 */
public class PluginRegistry {
	
	private static PluginRegistry _instance = null;
	private List<PluginDescriptor> _chain;
	private Map<PluginDescriptor, IProductIDPlugin> _cache;
	
	/**
	 * Private constructor.
	 */
	private PluginRegistry() {
		_chain = new ArrayList<PluginDescriptor>();
		_cache = new HashMap<PluginDescriptor, IProductIDPlugin>();
		loadConfig();
	}
	
	/**
	 * 
	 * @return The one and only instance of the plugin registry.
	 */
	public static PluginRegistry instance() {
		if (_instance == null) {
			_instance = new PluginRegistry();
		}
		
		return _instance;
	}
	
	/**
	 * Private method called on construction that loads data from the config file
	 * using ConfigProcessor.
	 */
	private void loadConfig() {
		ConfigProcessor cp = new ConfigProcessor();
		try {
			_chain = cp.parseConfig();
		}
		catch (IOException e) {
			//I'm assuming the file isn't there.  Ignore and move on.
		}
	}
	
	/**
	 * 
	 * @return The chain of plugins contained in this registry.
	 */
	public List<PluginDescriptor> getChain() {
		return _chain;
	}
	
	/**
	 * Spawns an instance of a particular plugin.
	 * @param pd The PluginDescriptor that describes a particular plugin.
	 * @return An instance of the plugin requested.
	 * @throws NotFoundException if the given PluginDescriptor is not in the registry.
	 */

	public IProductIDPlugin spawnPlugin(PluginDescriptor pd) throws NotFoundException {	
		URL pluginURL = null;
		IProductIDPlugin plugin = null;
		URLClassLoader loader = (URLClassLoader)ClassLoader.getSystemClassLoader();
		
		//only load plugins that are in the chain
		if (!_chain.contains(pd)) {
			throw new NotFoundException("Plugin not in the registry!");
		}
		
		//if we've already instantiated this class, don't do it again
		if (_cache.containsKey(pd)) {
			return _cache.get(pd);
		}
		
		//construct the plugin folder URL based on the current working directory
		URL curDir = this.getClass().getProtectionDomain().getCodeSource().getLocation();
		String pluginDir = curDir.getFile();
		if (pluginDir.endsWith("/bin/")) {
			//running via Eclipse
			pluginDir = pluginDir.substring(0, pluginDir.length() - 4);
		}
		else if (pluginDir.endsWith(".jar")) {
			//running from a jar file
			pluginDir = pluginDir.substring(0, (pluginDir.lastIndexOf('/') + 1));
		}
		pluginDir = "file:/" + pluginDir.substring(1) + "plugins/";
		//System.err.println(pluginDir);
		try {
			pluginURL = new URL(pluginDir);
		} 
		catch (MalformedURLException e) {
			//this happens when pluginDir isn't a valid directory.
			//in this case, we can ignore this and not add the plugin directory
			e.printStackTrace();
		}
		
		//now try to load up the plugin using reflection, and put it into the cache if successful
		try (CustomClassLoader myLoader = new CustomClassLoader(loader.getURLs())) {
			if (pluginURL != null) myLoader.addURL(pluginURL);
			Class<?> c = myLoader.loadClass(pd.getClassName());
			plugin = (IProductIDPlugin)c.newInstance();
			_cache.put(pd, plugin);
		} 
		catch (ClassNotFoundException e) {
			//This occurs when the plugin can't be found in the classpath
			e.printStackTrace();
			throw new NotFoundException("Plugin not found!");
		}
		catch (InstantiationException | IllegalAccessException e) {
			//this occurs when the plugin doesn't implement the interface properly
			e.printStackTrace();
			throw new NotFoundException("Plugin can't be understood!");
		} 
		catch (IOException e) {
			//this occurs when Java can't auto-close the class loader.  Ignore it.
			e.printStackTrace();
		}
		assert(plugin != null);
		
		//we're done!  That was intense.
		return plugin;
	}
	
	@SuppressWarnings("unused")
	private void printClasspath(ClassLoader loader) {
		URL[] urls = ((URLClassLoader) loader).getURLs();

		for (int i = 0; i < urls.length; i++) {
			System.err.println(urls[i].getFile());
		}

	}
	
	/**
	 * A method to be only used for testing.
	 * @param pd
	 */
	protected void addToChain(PluginDescriptor pd) {
		_chain.add(pd);
	}
	
	/**
	 * This class is really just a wrapper around the system classloader so PluginRegistry can
	 * add paths to the classpath.  If URLClassLoader.addURL() wasn't a protected method, I
	 * wouldn't need this.  Oh well.
	 * @author Royce
	 *
	 */
	private class CustomClassLoader extends URLClassLoader {
		/**
		 * @inheritDoc
		 */
		public CustomClassLoader(URL[] arg0) {
			super(arg0);
		}

		/**
		 * @inheritDoc
		 */
		public CustomClassLoader(URL[] arg0, ClassLoader arg1) {
			super(arg0, arg1);
		}

		/**
		 * @inheritDoc
		 */
		public CustomClassLoader(URL[] arg0, ClassLoader arg1, URLStreamHandlerFactory arg2) {
			super(arg0, arg1, arg2);
		}
		
		@Override
		/**
		 * @inheritDoc
		 */
		public void addURL(URL url) {
			super.addURL(url);
		}
	}
	
}
